package org.axed.user.client;

import java.util.ArrayList;

import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Window;

import com.google.gwt.core.client.GWT;
import org.axed.user.client.impl.CursesElementImpl;

/**
 * Provides a HTML element with logic to determine the coordinates
 * of innerText(innerHTML) and viceversa determine the position of 
 * coords in the text.
 */
public class CursesElement {
	/**
	 * Native implementation workarounds some specific errors with IE.
	 */
	private static CursesElementImpl impl;
  	static {
    	impl = (CursesElementImpl) GWT.create(CursesElementImpl.class);
  	}

   	/**
	 * That element had the cursor-split set last.
	 */
	private static CursesElement lastCursor = null;

	/**
 	 * Buffer to receive coordinates from called subroutines.
 	 */
	private static Coords coords = new Coords();

	/**
	 * The htmlelement
	 */
	private Element element;
	
	/**
	 * The inner text. 
	 */
	public HtmlString text;

	/**
	 * The text's version of the latest cache
	 */
	int cversion;

	/**
	 * The top coords of the cache
	 */
	int[] cTop = null;

	/**
	 * The left coords of the cache
	 */
	int[] cLeft = null;

	/**
	 * The left coords of the cache
	 */
	int[] cHeight = null;

	/**
	 * this will be appended to the IntexString; 
	 */
	private String suffix = "";

	/**
	 * Constructor.
	 */
	public CursesElement() {
		this(new HtmlString());
	}

	/**
	 * Constructor.
	 */
	public CursesElement(HtmlString text) {
		this.text = text;
		element = DOM.createDiv();	
		//DOM.setStyleAttribute(element, "whiteSpace", "pre-line");
		//DOM.setStyleAttribute(element, "textWrap", "unrestricted");
		DOM.setStyleAttribute(element, "wordWrap", "break-word");
		cversion = -1;
	}

	/**
	 * Constructor.
	 *
	 * @param text  The inner text for this element.
	 */
	public CursesElement(String text) {
		this.text = new HtmlString(text);
		cversion = -1;
	}

	/**
	 * Finds the entry pos where the line wraps
	 */
	public int findWrap(int targetTop, int start, boolean forward) {
		return findEntryPos(targetTop, Integer.MAX_VALUE, start, forward);
	}

	/**
	 * Finds the entry pos from a line for given top and left coordinates.
	 * Parameters start and forward give hints to get a faster solution.
	 *
	 * @param targetTop     the top coord to find.
	 * @param targetLeft    the left coord to find.
	 * 
	 * @param start         the column to start looking
	 * @param forward       true -> looking forward, false -> looking backward
	 */
	 public int findEntryPos(int targetTop, int targetLeft, int start, boolean forward) {
		//AxedArea.debug(true, "findColPos(" + targetTop + ", " + targetLeft + ", " + start + ", " + forward + ")");
		//GWT.log("findColPos(" + targetTop + ", " + targetLeft + ", " + start + ", " + forward + ")", null);
		//int jump = doc.getEditWidth()/doc.getColWidth() - 1;
		int jump = 50;
		int c = start + (forward ? jump : -jump);
		int cmax = text.entries();
		int lastxdiff = 0;
		int lastydiff = 0;
		if (c < 0) {
			c = 0;
		}
		if (c > cmax) {
			c = cmax;
		}
		while (jump >= 1) {
			lastxdiff = coords.left - targetLeft;
			lastydiff = coords.top - targetTop;
			getCoords(c, coords);
			//AxedArea.debug(true, "  c: " + c + ", jump: " + jump + " coords.top:" + coords.top + ", targetTop:" + targetTop);
			if (coords.top > targetTop + 1) {
				// in a line too far down
				if (c == 0) {
					// TODO should not happen
					break;
				}
				if (forward) {
					forward = false;
					jump /= 2;
				}
				c = c - jump;
				if (c < 0) {
					c = 0;
				}
				lastxdiff = 0;
				continue;
			}

			if (coords.top < targetTop) {
				// jumped more than one line
				if (c == cmax) {
					// TODO should not happen
					break;
				}
				if (!forward) {
					forward = true;
					jump /= 2;
				}
				c = c + jump;
				if (c > cmax) {
					c = cmax;
				}
				lastxdiff = 0;
				continue;
			}
			//AxedArea.debug(true, "  heightmatch, coords.left:" + coords.left + ", targetLeft:" + targetLeft);

			// here the line matches
			if (coords.left < targetLeft) {
				// too left
				if (c == cmax) {
					break;
				}
				if (!forward) {
					forward = true;
					jump /= 2;
				}
				c = c + jump;
				if (c > cmax) {
					c = cmax;
				}
				continue;
			}

			if (coords.left > targetLeft) {
				// too right
				if (c == 0) {
					break;
				}
				if (forward) {
					forward = false;
					jump /= 2;
				}
				c = c - jump;
				if (c < 0) {
					c = 0;
				}
				continue;
			}
			// exact hit;
			lastxdiff = 0;
			break;
		}
	
		// check if +/- 1 would be closer to the target
		if (lastxdiff != 0) {
			if (Math.abs(coords.left - targetLeft) > Math.abs(lastxdiff)) {
				//GWT.log("x-correcting by " + (forward ? 1 : -1), null);
				c += forward ? 1 : -1;
			}
		}

		if (Math.abs(coords.top - targetTop) > Math.abs(lastydiff)) {
			//GWT.log("y-correcting by " + (forward ? 1 : -1), null);
			c += forward ? 1 : -1;
		}

		if (c < 0) {
			c = 0;
		}
		if (c > cmax) {
			c = cmax;
		}
		return c;
	}


	/**
	 * Returns the vertical bottom position of the line.
	 */
	public int getBottom() {
		return DOM.getElementPropertyInt(element, "offsetTop") + 
		       DOM.getElementPropertyInt(element, "offsetHeight"); 
	} 

	/**
	 * Retrieves the coordinates of an entry, that is before the entry starts.
	 *
	 * Take care this routine modifies the DOM to retrieve the position, thus it takes quite
	 * some CPU-load. Don't use unnecessarly.
	 *
	 * @param entry  Entry to retrieve the coordinates from.
	 * @param answer The answer will be stored into this object.
	 */
	public void getCoords(int entry, Coords answer) {
//		if (text.version != cversion) {
//			newCache();
//		}
//		if (cTop[entry] != -1) {
//			answer.top = cTop[entry];
//			answer.left = cLeft[entry];
//			answer.height = cHeight[entry];
//			return;
//		}
		doGetCoords(entry, answer);
//		cTop[entry] = answer.top;
//		cTop[entry] = answer.left;
//		cTop[entry] = answer.height;
	}

	/**
	 * Returns the element. 
	 */
	public Element getElement() {
		return element;
	}


	/**
	 * Returns the total height of the line.
	 */
	public int getHeight() {
		return DOM.getElementPropertyInt(element, "offsetHeight"); 
	}

	/**
	 * Returns the horizontal left position of the line.
	 */	
	public int getLeft() {
		return DOM.getElementPropertyInt(element, "offsetLeft"); 
	} 

	/**
	 * Returns the vertical top position of the line.
	 */	
	public int getTop() {
		return DOM.getElementPropertyInt(element, "offsetTop"); 
	} 

	public void refresh() {
		if (text.hasCharacters()) {
			DOM.setInnerHTML(element, text.toString() + suffix);
		} else {
			DOM.setInnerHTML(element, "&nbsp;" + suffix);
		}
	}

	/**
	 * The suffix will be appended to the htmlString.
	 * User e.g. for &lt;br/&gt;
	 */
	public void setSuffix(String suffix) {
		this.suffix = suffix;
		refresh();
	}

//privates

	/**
	 * Add a span on this entry.
	 */
	private Element setCursor(int entry) {
		if (lastCursor != null && lastCursor != this) {
			// remove the cursor split from the last cursor holder
			lastCursor.refresh();
		}
		lastCursor = this;
		String s;
		if (entry >= 0) {
			if (!AxedArea.DEBUG_CURSOR_SET) {
				s = text.embraceEntry(entry, "<span id=\"curses\" style=\"text-wrap: none\">", "</span>");
			} else {
				s = text.embraceEntry(entry, "<span id=\"curses\" style=\"background-color:#66FF66\">", "</span>");
			}
		} else {
			// entry=-1 means its an empty line.
			if (!AxedArea.DEBUG_CURSOR_SET) {
				s = "<span id=\"curses\">&nbsp;</span>";
			} else {
				s = "<span id=\"curses\" style=\"background-color:#66FF66\">&nbsp;</span>";
			}
		}
		s += suffix;
		DOM.setInnerHTML(element, s);
		return DOM.getElementById("curses");
	}

	/**
	 * TODO
	 */
	private void doGetCoords(int entry, Coords answer) {
		Element cursor; 
		boolean before = true;
		int ts = text.entries();
		int e = entry;

		if (text.hasCharacters()) {
			char tc;
			if (entry < ts) {
				tc = text.getType(entry);
				// find the first non-empty entry after pos
				while (tc == 'x' && e < ts) {
					tc = text.getType(e);
					if (tc != 'x') {
						break;
					}
					e++;
				}
			} else {
				// position at end of line.
				tc = 'x';
			}

			if (tc != 'c') {
				// the first non-empty entry is either a space or end of string
				// position after the last entry instead.
				e = entry - 1;
				tc = text.getType(e);
				before = false;
				while (tc == 'x' && e >= 0) {
					tc = text.getType(e);
					if (tc != 'x') {
						break;
					}
					e--;
				}
				if (tc != 'c') {
					throw new AxedInternalError("CursesElement: can't neither position before or after.");
				}
			}
			cursor = setCursor(e);
		} else {
			cursor = setCursor(-1);
		}
		
		int l, t, h;

		if (before) {
			// position before
	   		l = DOM.getElementPropertyInt(cursor, "offsetLeft");
			t = DOM.getElementPropertyInt(cursor, "offsetTop");
			h = DOM.getElementPropertyInt(cursor, "offsetHeight");
		} else {
			// position after
			l  = DOM.getElementPropertyInt(cursor, "offsetWidth");
	   		l += DOM.getElementPropertyInt(cursor, "offsetLeft");
			t  = DOM.getElementPropertyInt(cursor, "offsetTop");
			h  = DOM.getElementPropertyInt(cursor, "offsetHeight");
		}

		// Workaround stupid IE bug
		t = impl.fixCoords(t, text.getTypes(), e, h, l);

//		AxedWidget.debug(true, " D:" + before +
//		                       " I: >" + DOM.getInnerHTML(cursor) + "<" + 
//		                       " T:" + DOM.getElementPropertyInt(cursor, "offsetTop") + 
//		                       " H:" + DOM.getElementPropertyInt(cursor, "offsetHeight") + 
//		                       " L:" + DOM.getElementPropertyInt(cursor, "offsetLeft"));
		if (l < 0) {
			l = 0;
		}
		answer.left   = l;
		answer.top    = t;
		answer.height = h;
	}

	/**
	 * TODO
	 */
	private void newCache() {
		int es = text.entries() + 1;
		cTop = new int[es];
		cLeft = new int[es];
		cHeight = new int[es];
		for (int i = 0; i < es; i++) {
			cTop[i] = -1;
			cLeft[i] = -1;
			cHeight[i] = -1;
		}
		cversion = text.version;
	}
}

